package com.homedo.async.mysql.support.meta;

import com.google.common.collect.Maps;
import com.homedo.async.mysql.support.dao.TypeChecker;
import com.homedo.async.mysql.support.convertor.forcolumnname.*;
import com.homedo.async.mysql.support.convertor.forcolumnval.*;
import com.homedo.async.mysql.support.convertor.forfieldval.ResultFieldValConvertorUtil;
import io.vertx.sqlclient.Row;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import org.reflections.ReflectionUtils;
import com.homedo.plainly.logger.Logger;
import com.homedo.plainly.logger.LoggerFactory;

import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import java.lang.reflect.Field;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collector;
import java.util.stream.Collectors;

/**
 * @author jgshun
 * @date 2020/1/19 7:30 PM
 */
@Getter
@EqualsAndHashCode
@ToString
public class EntityMetaData<T> {
    private static final Logger _LOGGER = LoggerFactory.getLogger(EntityMetaData.class);

    //实体类型
    private Class<T> entityClazs;
    //表名
    private String tableName;
    //主键属性
    private Field primaryKeyField;
    //实体属性集合
    private Set<Field> entityFields;

    private boolean primaryAutoGenerate;

    //实体属性与列名的对应关系
    private Map<Field, String> fieldColumNameMapping = Maps.newHashMap();
    //实体属性名称与列表的对应关系
    private Map<String, String> fieldNameColumnNameMapping = Maps.newHashMap();

    //列名对应的转换器
    private Map<String, ConditionColumnNameConvertorI> columnNameConvertorMapping = Maps.newHashMap();
    //列值对应的转换器
    private Map<String, ConditionColumnValConvertorI> columnValConvertorMapping = Maps.newHashMap();

    //根据列名取出列值并转换成映射对象--收集器
    private Collector<Row, ?, List<T>> collector;

    public EntityMetaData(Class<T> entityClazs) {
        this.entityClazs = entityClazs;
        init();
    }

    private void init() {
        this.collectTableName();
        this.collectEntityFields();
        if (entityFields != null && entityFields.size() > 0) {
            this.collectPrimaryKeyFieldName();
            this.collectFieldColumnMapping();
            this.collectColumnNameConvertorMapping();
            this.collectColumnValConvertorMapping();
            this.collector = this.createCollector();
        }
    }

    private Collector<Row, ?, List<T>> createCollector() {

        Collector<Row, ?, List<T>> collector = Collectors.mapping(
                row -> {
                    try {
                        T entity = entityClazs.newInstance();

                        Iterator<Map.Entry<Field, String>> fieldColumNameIterable = fieldColumNameMapping.entrySet().iterator();

                        while (fieldColumNameIterable.hasNext()) {
                            Map.Entry<Field, String> fieldColumNameEntry = fieldColumNameIterable.next();
                            Field field = fieldColumNameEntry.getKey();
                            String columnName = fieldColumNameEntry.getValue();

                            Object valObj = row.getValue(columnName);

                            Object finalVal = ResultFieldValConvertorUtil.convertor(field.getType(), valObj);

                            field.setAccessible(true);
                            field.set(entity, finalVal);
                        }
                        return entity;
                    } catch (InstantiationException | IllegalAccessException ex) {
                        _LOGGER.error("[创建实例失败，请确保有无参构造器：{}]", ex.getMessage(), ex);
                        throw new RuntimeException("[创建实例失败，请确保有无参构造器]");
                    }

                },
                Collectors.toList()
        );

        return collector;
    }

    private void collectColumnValConvertorMapping() {
        Set<Map.Entry<Field, String>> entries = this.fieldColumNameMapping.entrySet();

        entries.forEach(e -> {
            Class fieldClz = e.getKey().getType();
            TypeChecker.TypeEnum type = TypeChecker.checkType(fieldClz);

            if (type == TypeChecker.TypeEnum.PRIMITIVE) {
                columnValConvertorMapping.put(e.getValue(), PrimitiveConditionColumnValConvertor.getInstance());
            } else if (type == TypeChecker.TypeEnum.NUMERIC) {
                columnValConvertorMapping.put(e.getValue(), NumericConditionColumnValConvertor.getInstance());
            } else if (type == TypeChecker.TypeEnum.DATE) {
                columnValConvertorMapping.put(e.getValue(), DateConditionColumnValConvertor.getInstance());
            } else if (type == TypeChecker.TypeEnum.STRING) {
                columnValConvertorMapping.put(e.getValue(), StringConditionColumnValConvertor.getInstance());
            } else if (type == TypeChecker.TypeEnum.JSON) {
                columnValConvertorMapping.put(e.getValue(), JsonConditionColumnValConvertor.getInstance());
            } else if (type == TypeChecker.TypeEnum.BUFFER) {
                columnValConvertorMapping.put(e.getValue(), BufferConditionColumnValConvertor.getInstance());
            } else if (type == TypeChecker.TypeEnum.UNKNOWN) {
                columnValConvertorMapping.put(e.getValue(), CommonConditionColumnValConvertor.getInstance());
            }

        });
    }

    private void collectColumnNameConvertorMapping() {
        Set<Map.Entry<Field, String>> entries = this.fieldColumNameMapping.entrySet();

        entries.forEach(e -> {
            Class fieldClz = e.getKey().getType();
            TypeChecker.TypeEnum type = TypeChecker.checkType(fieldClz);

            if (type == TypeChecker.TypeEnum.PRIMITIVE) {
                columnNameConvertorMapping.put(e.getValue(), PrimitiveConditionColumnNameConvertor.getInstance());
            } else if (type == TypeChecker.TypeEnum.NUMERIC) {
                columnNameConvertorMapping.put(e.getValue(), NumericConditionColumnNameConvertor.getInstance());
            } else if (type == TypeChecker.TypeEnum.DATE) {
                columnNameConvertorMapping.put(e.getValue(), DateConditionColumnNameConvertor.getInstance());
            } else if (type == TypeChecker.TypeEnum.STRING) {
                columnNameConvertorMapping.put(e.getValue(), StringConditionColumnNameConvertor.getInstance());
            } else if (type == TypeChecker.TypeEnum.JSON) {
                columnNameConvertorMapping.put(e.getValue(), JsonConditionColumnNameConvertor.getInstance());
            } else if (type == TypeChecker.TypeEnum.BUFFER) {
                columnNameConvertorMapping.put(e.getValue(), BufferConditionColumnNameConvertor.getInstance());
            } else if (type == TypeChecker.TypeEnum.UNKNOWN) {
                columnNameConvertorMapping.put(e.getValue(), CommonConditionColumnNameConvertor.getInstance());
            }

        });

    }

    private void collectEntityFields() {
        this.entityFields = ReflectionUtils.getAllFields(this.entityClazs);
    }

    private void collectPrimaryKeyFieldName() {
        for (Field field : this.entityFields) {
            Id id = field.getDeclaredAnnotation(Id.class);
            if (id == null) {
                continue;
            }
            if (this.primaryKeyField != null) {
                throw new RuntimeException("[主键重复：" + field + "]");
            }
            this.primaryKeyField = field;
        }

        GeneratedValue generatedValue = this.primaryKeyField.getDeclaredAnnotation(GeneratedValue.class);
        if (generatedValue != null) {
            this.primaryAutoGenerate = true;
        }
    }

    private void collectFieldColumnMapping() {
        for (Field field : this.entityFields) {
            Column column = field.getDeclaredAnnotation(Column.class);
            if (column == null) {//没有标注是数据库列的自动忽略掉
                continue;
            }
            String columnName = column.name().isEmpty() ? field.getName() : column.name();

            this.fieldNameColumnNameMapping.put(field.getName(), columnName);
            this.fieldColumNameMapping.put(field, columnName);
        }
    }

    private void collectTableName() {
        Table table = this.entityClazs.getDeclaredAnnotation(Table.class);
        this.tableName = table == null ? this.entityClazs.getSimpleName() :
                table.name().isEmpty() ? this.entityClazs.getSimpleName() : table.name();
    }

}
